﻿//-----------------------------------------------------------------------
// <copyright file="DisposableFieldShouldBeDisposed.cs">
//     Copyright (c) Nicole Calinoiu. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;

using Microsoft.FxCop.Sdk;
using Microsoft.VisualStudio.CodeAnalysis.Extensibility;

using Bordecal.FxCop.Rules.Usage.Configuration;
using Bordecal.FxCop.Sdk;

namespace Bordecal.FxCop.Rules.Usage
{
	/// <summary>
	/// Rule that verifies that a disposable field is disposed from the parent type's Dispose method.
	/// </summary>
	internal sealed class DisposableFieldShouldBeDisposed : ConfigurableIntrospectionRule<DisposableFieldShouldBeDisposedConfiguration>
	{
		#region Constructors

		/// <summary>
		/// Initializes a new instance of the <see cref="DisposableFieldShouldBeDisposed"/> class.
		/// </summary>
		public DisposableFieldShouldBeDisposed()
			: base(RuleDescriptorFactory.CreateSingleResolutionDescriptor<DisposableFieldShouldBeDisposed>(
				RuleCategory.Usage,
				"Dispose disposable fields",
				"If a type implements IDisposable, each of its disposable fields should be disposed during disposition of the parent type.",
				75,
				FixCategories.DependsOnFix,
				MessageLevel.Warning,
				"Change the Dispose method on type {0} to call Dispose on the disposable {1} field."))
		{
		}

		#endregion

		#region Properties

		private TypeNode IContainerType { get; set; }

		private TypeNode FocalType { get; set; }

		private Method FocalMethod { get; set; }

		private bool InDisposeMethod { get; set; }

		private HashSet<Field> DisposableFields { get; set; }

		private HashSet<Field> DisposedFields { get; set; }

		private IDictionary<Method, Field> AutoPropertyGetterBackingFields { get; set; }

		#endregion

		#region Methods

		/// <summary>
		/// Performs pre-analysis setup tasks.
		/// </summary>
		public override void BeforeAnalysis()
		{
			base.BeforeAnalysis();

			this.IContainerType = FrameworkAssemblies.System.GetType(Identifier.For("System.ComponentModel"), Identifier.For("IContainer"));
		}

		/// <summary>
		/// Verifies compliance of a type with the implemented rule.
		/// </summary>
		/// <param name="type">The target <see cref="TypeNode"/>.</param>
		/// <returns>A collection of found problems.</returns>
		[SuppressMessage("Microsoft.StyleCop.CSharp.MaintainabilityRules", "SA1119:StatementMustNotUseUnnecessaryParenthesis",
			Justification = "The parentheses increase readability.")]
		public override ProblemCollection Check(TypeNode type)
		{
			if (type == null)
			{
				throw new ArgumentNullException("type");
			}

			if (type.IsAssignableTo(FrameworkTypes.IDisposable))
			{
				this.FocalType = type;
				this.DisposableFields = new HashSet<Field>();
				this.DisposedFields = new HashSet<Field>();

				this.AutoPropertyGetterBackingFields = DisposableFieldShouldBeDisposed.GetAutoPropertyGetterBackingFields(type);

				foreach (Member member in type.Members)
				{
					Field field = member as Field;
					if (field != null)
					{
						this.VisitField(field);
					}
					else
					{
						Method method = member as Method;
						if (method != null)
						{
							this.InDisposeMethod = (RuleUtilities.IsDisposeBool(method) || RuleUtilities.IsDisposeMethod(method));
							this.FocalMethod = method;
							this.VisitBlock(method.Body);
							this.InDisposeMethod = false;
						}
					}
				}

				this.RemoveComponentFields();

				foreach (Field undisposedField in this.DisposableFields.Except(DisposableFieldShouldBeDisposed.GetNonPrivateAutoPropertySetterFields(type)).Except(this.DisposedFields))
				{
					this.AddProblem(undisposedField, undisposedField.Name.Name, FixCategories.DependsOnFix, type, undisposedField);
				}
			}

			return this.Problems;
		}

		private static IEnumerable<Field> GetNonPrivateAutoPropertySetterFields(TypeNode type)
		{
			IList<Field> result = new List<Field>();
			foreach (PropertyNode property in type.Members.Where(m => m.NodeType == NodeType.Property).Select(m => (PropertyNode)m))
			{
				if ((property.Setter != null) && (!property.Setter.IsPrivate))
				{
					Field backingField = DisposableFieldShouldBeDisposed.GetGeneratedBackingField(property);
					if (backingField != null)
					{
						result.Add(backingField);
					}
				}
			}

			return result;
		}

		private static IDictionary<Method, Field> GetAutoPropertyGetterBackingFields(TypeNode type)
		{
			IDictionary<Method, Field> result = new Dictionary<Method, Field>();
			foreach (PropertyNode property in type.Members.Where(m => m.NodeType == NodeType.Property).Select(m => (PropertyNode)m))
			{
				Field backingField = DisposableFieldShouldBeDisposed.GetGeneratedBackingField(property);
				if (backingField != null)
				{
					result.Add(property.Getter, backingField);
				}
			}

			return result;
		}

		private static Field GetGeneratedBackingField(PropertyNode property)
		{
			string fieldName = string.Format(CultureInfo.InvariantCulture, "<{0}>k__BackingField", property.Name.Name);

			Field field = property.DeclaringType.GetField(Identifier.For(fieldName));
			if ((field != null) && (!RuleUtilities.HasCustomAttribute(field, Types.CompilerGeneratedAttribute, false)))
			{
				field = null;
			}

			return field;
		}

		private static bool IsPropertyGetterFromOtherType(Expression expression)
		{
			bool result = false;

			MethodCall call = expression as MethodCall;
			if (call != null)
			{
				MemberBinding binding = (MemberBinding)call.Callee;
				result = (binding.TargetObject.NodeType != NodeType.This) && (((Method)binding.BoundMember).DeclaringMember != null);
			}

			return result;
		}

		private static bool IsDisposeMethod(Member member)
		{
			bool result = RuleUtilities.IsDisposeMethod(member);
			if (!result)
			{
				Method method = member as Method;
				if (method != null)
				{
					if (method.DeclaringType.IsAssignableTo(FrameworkTypes.IDisposable))
					{
						if (method.Name.Name.Equals("Close", StringComparison.OrdinalIgnoreCase))
						{
							result = true;
						}
					}
				}
			}

			return result;
		}

		private void RemoveComponentFields()
		{
			if (this.DisposedFields.Any(f => f.Type.IsAssignableTo(this.IContainerType)))
			{
				this.DisposableFields.RemoveWhere(f => f.Type.IsAssignableTo(FrameworkTypes.IComponent));
			}
		}

		/// <summary>
		/// Visits a <see cref="AssignmentStatement"/>.
		/// </summary>
		/// <param name="assignment">The <see cref="AssignmentStatement"/>.</param>
		public override void VisitAssignmentStatement(AssignmentStatement assignment)
		{
			if (assignment == null)
			{
				throw new ArgumentNullException("assignment");
			}

			base.VisitAssignmentStatement(assignment);

			MemberBinding target = assignment.Target as MemberBinding;
			if (target != null)
			{
				Field field = target.BoundMember as Field;
				if ((field != null) && (!field.IsStatic) && (field.DeclaringType == this.FocalType))
				{
					if (!this.DisposableFields.Contains(field))
					{
						if (assignment.Source.Type.IsAssignableTo(FrameworkTypes.IDisposable) && (!DisposableFieldShouldBeDisposed.IsPropertyGetterFromOtherType(assignment.Source)))
						{
							this.DisposableFields.Add(field);
						}
					}
				}
			}
		}

		/// <summary>
		/// Visits a method call.
		/// </summary>
		/// <param name="call">The target <see cref="MethodCall"/>.</param>
		public override void VisitMethodCall(MethodCall call)
		{
			if (call == null)
			{
				throw new ArgumentNullException("call");
			}

			base.VisitMethodCall(call);

			if (this.InDisposeMethod)
			{
				MemberBinding callee = (MemberBinding)call.Callee;
				if (DisposableFieldShouldBeDisposed.IsDisposeMethod(callee.BoundMember))
				{
					this.VisitDisposeMethodTarget(callee.TargetObject);
				}
				else if (this.CurrentConfiguration.DispositionHelperMethodNames.Contains(callee.BoundMember.FullName, StringComparer.Ordinal))
				{
					foreach (Expression operand in call.Operands)
					{
						if (operand.NodeType == NodeType.Local)
						{
							Local local = (Local)operand;
							if (RuleUtilities.IsCompilerGenerated(local))
							{
								foreach (Expression element in FxCopUtility.GetParameterArrayElements(local, this.FocalMethod.Body).Values)
								{
									this.VisitDisposeMethodTarget(element);
								}
							}
						}
						else
						{
							this.VisitDisposeMethodTarget(operand);
						}
					}
				}
			}
		}

		private void VisitDisposeMethodTarget(Expression target)
		{
			MemberBinding disposedMemberBinding = target as MemberBinding;
			if (disposedMemberBinding != null)
			{
				Member disposedMember = disposedMemberBinding.BoundMember;
				if ((disposedMember.DeclaringType == this.FocalType) && (disposedMember.NodeType == NodeType.Field))
				{
					this.DisposedFields.Add((Field)disposedMember);
				}
			}
			else
			{
				MethodCall getterCall = target as MethodCall;
				if (getterCall != null)
				{
					MemberBinding getterBinding = (MemberBinding)getterCall.Callee;
					Member getterMember = getterBinding.BoundMember;
					if ((getterMember.DeclaringType == this.FocalType) && (getterMember.NodeType == NodeType.Method))
					{
						Field field = this.AutoPropertyGetterBackingFields[(Method)getterMember];
						if (field != null)
						{
							this.DisposedFields.Add(field);
						}
					}
				}
			}
		}

		#endregion
	}
}